package lib.messaging;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.List;

import lib.ArgumentValidator;
import lib.logging.ILogger;
import acm.util.RandomGenerator;

/**
 * Handles all socket communications
 * @implements {@link ISender} for sending messages
 * @implements {@link IReceiver} for receiving messages
 * @author abhip
 *
 */
public class Communicator implements IReceiver, ISender
{

    private ILogger logger;
    private double packetDropProbability;

    private RandomGenerator randomGenerator;

    public Communicator(double packetDropProbability, ILogger logger)
    {
        ArgumentValidator.IsBetweenBothInclusive("packetDropProbability", packetDropProbability, 0.0, 1.0);
        ArgumentValidator.IsNotNull("logger", logger);

        this.packetDropProbability = packetDropProbability;
        this.randomGenerator = RandomGenerator.getInstance();
        this.logger = logger;
    }

    /**
     * Opens a socket and listens for incoming messages for infinite time
     * When receives a message, returns the message read.
     * @return message read as byte array
     * @throws IOException
     * @throws ClassNotFoundException
     */
    @Override
    public byte[] ReceiveBytes(int port) throws IOException, ClassNotFoundException
    {
        return ReceiveBytes(port, 0);
    }

    /**
     * Opens a socket and listens for incoming messages till specified timeout
     * When receives a message, returns the message read.
     * @return message read as byte array
     * @throws IOException
     * @throws ClassNotFoundException
     */
    @Override
    public byte[] ReceiveBytes(int port, int timeout) throws IOException, ClassNotFoundException
    {
        ArgumentValidator.IsGreaterThan("port", port, 0);
        ArgumentValidator.IsGreaterThanOrEqualTo("timeout", timeout, 0);

        ServerSocket serverSocket = null;
        Socket socket = null;
        ObjectInputStream inStream = null;
        byte[] data = null;

        try
        {
            // Create server socket
            serverSocket = new ServerSocket(port);

            // Wait for connection
            serverSocket.setSoTimeout(timeout);
            socket = serverSocket.accept();

            // Get input stream
            inStream = new ObjectInputStream(socket.getInputStream());

            // Receive the data
            data = (byte[])inStream.readObject();
            return data;
        }
        finally
        {
            // Close connection
            if (inStream != null)
            {
                inStream.close();
            }

            if (serverSocket != null)
            {
                serverSocket.close();
            }
        }
    }

    @Override
    public void SendBytes(List<NetworkAddress> addresses, byte[] data)
            throws UnknownHostException, IOException
    {
        ArgumentValidator.IsNotNull("addresses", addresses);
        ArgumentValidator.IsNotNull("data", data);

        for (int i = 0; i < addresses.size(); ++i)
        {
            SendBytes(addresses.get(i), data);
        }
    }

    /**
     * Opens a network socket for communication and sends message over it
     * @param data Data to be sent
     * @throws IOException
     * @throws UnknownHostException
     */
    @Override
    public void SendBytes(NetworkAddress address, byte[] data) throws UnknownHostException, IOException
    {
        ArgumentValidator.IsNotNull("address", address);
        ArgumentValidator.IsNotNull("data", data);

        if (ShouldDropPacket())
        {
            logger.LogError(
                    "Oops, packet dropped! Destination:%s",
                    address.Name() + ":" + address.Port());
            return;
        }

        Socket socket = null;
        ObjectOutputStream outStream = null;

        try
        {
            // Create a socket to connect to the server
            socket = new Socket(address.Name(), address.Port());

            // Get output stream
            outStream = new ObjectOutputStream(socket.getOutputStream());

            // Send data to server
            outStream.writeObject(data);
            outStream.flush();
        }
        finally
        {
            // close connection
            if (outStream != null)
            {
                outStream.close();
            }

            if (socket != null && !socket.isClosed())
            {
                socket.close();
            }
        }
    }

    private boolean ShouldDropPacket()
    {
        return randomGenerator.nextBoolean(packetDropProbability);
    }
}
